package org.tamal.classinfo;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;


/**
 * This class reads one class file and extracts different information about that
 * class. It do not use reflection API.
 *
 * @author Tamal Kanti Nath
 */
public final class ClassInfo {

    private static final int MAGIC_NUMBER = 0xCAFEBABE;
    private int major;
    private int minor;
    private Constant[] pool;
    private int accessFlags;
    private int thisClass;
    private int superClass;
    private int[] interfaces;
    private Field[] fields;
    private Method[] methods;
    private Attribute[] attributes;

    /**
     * Reads class information from class file.
     * @param file the class-file
     */
    public ClassInfo(File file) {
        try (InputStream is = new FileInputStream(file)) {
            readClassInfo(is);
        } catch (IOException e) {
            throw new ClassFormatException(e);
        }
    }

    /**
     * Reads class information from input stream.<br/>
     * E.g. To read <code>{@link java.lang.Class}</code>:<br/><br/>
     * <code>InputStream stream =
     * getClass().getResourceAsStream("/java/lang/Class.class");<br/>
     * ClassInfo info = new ClassInfo(stream);</code>
     * @param is the stream containing class
     */
    public ClassInfo(InputStream is) {
        try {
            readClassInfo(is);
        } catch (IOException e) {
            throw new ClassFormatException(e);
        }
    }

    /**
     * Reads class information from the given URL.<br/>
     * E.g. To read <code>{@link java.lang.Class}</code>:<br/><br/>
     * <code>String url = System.getProperty("java.home");<br/>
     * url = "jar:file:" + url + "/lib/rt.jar!/java/lang/Class.class";
     * <br/>ClassInfo info = new ClassInfo(new URL(url));</code>
     *
     * @param url the URL
     */
    public ClassInfo(URL url) {
        try {
            readClassInfo(url.openStream());
        } catch (IOException e) {
            throw new ClassFormatException(e);
        }
    }

    /**
     * Reads class information from a class name.
     * @param className The class name
     */
    public ClassInfo(String className) {
        String name = '/' + className.replace('.', '/') + ".class";
        try {
            readClassInfo(getClass().getResourceAsStream(name));
        } catch (IOException e) {
            throw new ClassFormatException(e);
        }
    }

    /**
     * Returns Major Number.
     * @return the major
     * @see #getVersion()
     */
    public int getMajor() {
        return major;
    }

    /**
     * Returns Minor Number.
     * @return the minor
     * @see #getVersion()
     */
    public int getMinor() {
        return minor;
    }

    /**
     * Returns version number.
     * @return version number
     */
    public String getVersion() {
        String version;
        switch (major) {
        case 45:
            version = "1.1";
            break;
        case 46:
            version = "1.2";
            break;
        case 47:
            version = "1.3";
            break;
        case 48:
            version = "1.4";
            break;
        case 49:
            version = "5";
            break;
        case 50:
            version = "6";
            break;
        case 51:
            version = "7";
            break;
        case 52:
            version = "8";
            break;
        default:
            version = "unknown";
        }
        return version;
    }

    /**
     * Returns Constant Pool.
     * @return the pool
     */
    public Constant[] getPool() {
        return pool;
    }

    /**
     * Returns the classes referenced by current class.
     * @return the list of classes
     */
    public List<String> getReferencedClasses() {
        List<String> classes = new ArrayList<>();
        for (int i = 1; i < pool.length; ++i) {
            if (pool[i].getTag() != Constant.CONSTANT_CLASS
                || pool[i].toString().startsWith("[")) {
                continue;
            }
            classes.add(pool[i].toString());
        }
        return classes;
    }

    /**
     * Returns the methods referenced by current class.
     * @return the list of classes
     */
    public List<String> getDeclaredMethods() {
        List<String> classes = new ArrayList<>();
        for (int i = 1; i < pool.length; ++i) {
            if (pool[i].getTag() != Constant.CONSTANT_METHOD_REF) {
                continue;
            }
            classes.add(pool[i].toString());
        }
        return classes;
    }

    /**
     * Returns the interface methods referenced by current class.
     * @return the list of classes
     */
    public List<String> getDeclaredInterfaceMethods() {
        List<String> classes = new ArrayList<>();
        for (int i = 1; i < pool.length; ++i) {
            if (pool[i].getTag() != Constant.CONSTANT_INTERFACE_METHOD_REF) {
                continue;
            }
            classes.add(pool[i].toString());
        }
        return classes;
    }

    /**
     * Returns the class or interface fields referenced by current class.
     * @return the list of classes
     */
    public List<String> getDeclaredFields() {
        List<String> classes = new ArrayList<>();
        for (int i = 1; i < pool.length; ++i) {
            if (pool[i].getTag() != Constant.CONSTANT_FIELD_REF) {
                continue;
            }
            classes.add(pool[i].toString());
        }
        return classes;
    }

    /**
     * Returns raw access flags.
     * @return the raw access flags
     * @see java.lang.reflect.Modifier
     */
    public int getRawAccessFlags() {
        return accessFlags;
    }

    /**
     * Returns Access Flags.
     * @return the accessFlags
     * @see java.lang.reflect.Modifier
     */
    public String getAccessFlags() {
        return AccessFlag.getClassFlag(accessFlags);
    }

    /**
     * Returns this class index.
     * @return the this class index
     */
    public int getThisClassIndex() {
        return thisClass;
    }

    /**
     * Returns this class name.
     * @return the this class name
     */
    public String getThisClass() {
        return pool[thisClass].toString();
    }

    /**
     * Returns super class index.
     * @return the super class index
     */
    public int getSuperClassIndex() {
        return superClass;
    }

    /**
     * Returns super class name.
     * @return the super class
     */
    public String getSuperClass() {
        // no superclass for java.lang.Object.
        return superClass == 0 ? null : pool[superClass].toString();
    }

    /**
     * Returns interface indexes.
     * @return the interface indexes
     */
    public int[] getInterfacesIndexes() {
        return interfaces;
    }

    /**
     * Returns interface names.
     * @return the interface names
     */
    public String[] getInterfaces() {
        String[] interfaceNames = new String[interfaces.length];
        for (int i = 0; i < interfaceNames.length; ++i) {
            interfaceNames[i] = pool[interfaces[i]].toString();
        }
        return interfaceNames;
    }

    /**
     * Returns fields.
     * @return the fields
     */
    public Field[] getFields() {
        return fields;
    }

    /**
     * Returns Methods.
     * @return the methods
     */
    public Method[] getMethods() {
        return methods;
    }

    /**
     * Returns Attributes.
     * @return the attributes
     */
    public Attribute[] getAttributes() {
        return attributes;
    }

    @Override
    public String toString() {
        return getThisClass();
    }

    private void readClassInfo(InputStream is) throws IOException {
        DataInputStream in = new DataInputStream(is);
        if (in.readInt() != MAGIC_NUMBER) {
            throw new ClassFormatException(
                    "Magic number (0xCAFEBABE) mismatch.");
        }
        minor = in.readUnsignedShort();
        major = in.readUnsignedShort();
        if (major < 45 || major > 52) {
            throw new ClassFormatException("Invalid version.");
        }
        pool = Constant.readConstantPool(in);
        accessFlags = in.readUnsignedShort();
        thisClass = in.readUnsignedShort();
        superClass = in.readUnsignedShort();
        readInterfaces(in);
        fields = Field.readFields(in, this);
        methods = Method.readMethods(in, this);
        attributes = Attribute.readAttributes(in, this);
        try {
            in.readByte();
        } catch (EOFException e) {
            return;
        }
        throw new ClassFormatException("Extra information found");
    }

    private void readInterfaces(DataInputStream in) throws IOException {
        int interfacesCount = in.readUnsignedShort();
        interfaces = new int[interfacesCount];
        for (int i = 0; i < interfacesCount; ++i) {
            interfaces[i] = in.readUnsignedShort();
        }
    }

}
